class ThreeKingdoms {
    async init(wallet) {
        if (wallet == "onto") {
            if (!window.onto) {
                throw "no wallet";
            }

            window.web3 = new Web3Mine(onto);
        } else {
            if (window.ethereum) {
                window.web3 = new Web3Mine(ethereum);
            } else if (window.web3) {
                window.web3 = new Web3Mine(web3.currentProvider);
            } else {
                throw "no wallet";
            }
        }

        this.addrCanteen = "0xCDaf449ad8872B348ddd2bA203Da62732dd05F3D";
        this.addrCrossChainSTK = "0xe737172C0e0b0c8d9D20BC0830F4063f8B251091";
        this.addrDailyTask = "0x032d51FFdC7381D106116537E446AF1B676A3EF6";
        this.addrEquip = "0xCC9F8E3B0795e82B5A25a6BC3a89f79661cA7bfe";
        this.addrEquipPackage = "0xC647975452DaA887f232A47f658BC2BEF71A4F1E";
        this.addrEquipSplit = "0x3bD4Af5550a3b9e019Da0ad4B451E18054642595";
        this.addrHero = "0x2d36C7c7c28B1442C0F2eBb2015E469147845144";
        this.addrHeroPackage = "0x4E3841476aBfd4FE787428f2364ef023E17dd638";
        this.addrManager = "0x3043DfA3D3Aa6C2D6A1265FC2271924acC261d93";
        this.addrMarket2 = "0x6D7412c3316a6cC6E95d80fa5064E57CBa119a7e";
        this.addrMoneyToScore1 = "0xdC99Cdd6692d8a2cec8675EF03534a3C5a1D8A68";
        this.addrNickname = "0xeE6D2c5cFfe2ac0af5dceb0483C0ee8e55183140";
        this.addrOneToOne = "0xe4c8dCF18CBCFF08a3530525fecE7196c92531B6";
        this.addrRecycleBin = "0x7Ae24f13e0e0c6Af2a6DD3F2f4dCC179E8fD3E54";
        this.addrRewardStation = "0x1083b99ab8A17368a1bBb02896cCb1fCE1982ce4";
        this.addrScoreBoard = "0x7eaf08C36a8717B3BE8dCc94c90875269B7343d3";
        this.addrSlot = "0xfEf1A784DA44f61E2cE12400A10cA51c14E73D99";
        this.addrWaterMarginCardMine = "0x1473B4649F51190b71387CEa88F6C5DAAcCF11A3";

        this.ADDRESS_ZERO = "0x0000000000000000000000000000000000000000";

        this.DENO_INTEGER = new BigInteger("10000000000");
        this.DENO_DECIMAL = new BigDecimal("10000000000");

        this.QUANTITY_MIN = 1;
        this.QUANTITY_MAX = 50;

        this.eventMgr = new EventMgr();
        this.listenLog(ABIS.Card, this.addrHero, "Transfer");
        this.listenLog(ABIS.Card, this.addrEquip, "Transfer");

        this.addressToToken = {};
        for (let symbol in TKConfig.TOKEN) {
            let token = TKConfig.TOKEN[symbol];
            this.addressToToken[token.address.toLowerCase()] = token;
        }

        this.timeDeltaInited = false;
        this.timeDelta = 0;
        this.checkTime();
        setInterval(() => {
            this.checkTime();
        }, 20000);
    }

    async login() {
        let accounts = await web3.eth.requestAccounts();
        this.account = accounts[0];

        this.chainId = await web3.eth.getChainId();

        return this.account;
    }

    async checkTime() {
        let blockTime = await this.call({
            abi: ABIS.Manager,
            address: this.addrManager,
            method: "getTimestamp",
        });

        let timeDelta = blockTime * 1000 - Date.now() + 200;
        if (!this.timeDeltaInited || this.timeDelta < timeDelta) {
            this.timeDeltaInited = true;
            this.timeDelta = timeDelta;
        }
    }

    currMillis() {
        return Date.now() + this.timeDelta;
    }

    currSecond() {
        return Math.floor(this.currMillis() / 1000);
    }

    clone(source) {
        let target;

        if (typeof source == "object") {
            if (Array.isArray(source)) {
                target = [];
                for (let i in source) {
                    target[i] = this.clone(source[i]);
                }
            } else if (source === null) {
                target = null;
            } else if (source.constructor === RegExp) {
                target = source;
            } else {
                target = {};
                for (let key in source) {
                    target[key] = this.clone(source[key]);
                }
            }
        } else {
            target = source;
        }

        return target;
    }

    requestHttp(params) {
        return new Promise((resolve, reject) => {
            let xhr = new XMLHttpRequest();
            xhr.onreadystatechange = () => {
                if (xhr.readyState == 4) {
                    let response = xhr.responseText;

                    let json = null;
                    try {
                        json = JSON.parse(response);
                    } catch (e) {}

                    if (xhr.status == 200) {
                        resolve({
                            status: xhr.status,
                            response: response,
                            json: json,
                        });
                    } else {
                        reject({
                            status: xhr.status,
                            response: response,
                            json: json,
                        });
                    }
                }
            };

            if (params.headers) {
                for (let key in params.headers) {
                    xhr.setRequestHeader(key, params.headers[key]);
                }
            }

            xhr.open(params.method || "GET", params.url, true);
            xhr.send(params.data);
        });
    }

    getContract(abi, address) {
        return new web3.eth.Contract(abi, address);
    }

    call(params) {
        if (!params.contract) {
            params.contract = this.getContract(params.abi, params.address);
        }

        if (!params.args) {
            params.args = [];
        }

        return params.contract.methods[params.method].apply(this, params.args).call();
    }

    send(params) {
        if (!params.contract) {
            params.contract = this.getContract(params.abi, params.address);
        }

        if (!params.args) {
            params.args = [];
        }

        let ps = {
            chainId: this.chainId,
            from: this.account,
            value: params.value || 0,
            gasPrice: 3e9,
        };

        if (params.gas) {
            ps.gas = params.gas;
        }

        return params.contract.methods[params.method].apply(this, params.args).send(ps);
    }

    getTokenByAddress(address) {
        return this.addressToToken[address.toLowerCase()];
    }

    sign(message) {
        return web3.eth.personal.sign(message, this.account);
    }

    async listenLog(abi, address, eventName) {
        let contract = this.getContract(abi, address);
        let events = contract.events;
        let event = events[eventName];
        let eventId;

        for (let key in events) {
            if (key.startsWith("0x") && events[key] == event) {
                eventId = key;
                break;
            }
        }

        address = address.toLowerCase();
        let eventKey = address + "," + eventName;

        let lastBn = await web3.eth.getBlockNumber() - 1;
        while (true) {
            let currBn = await web3.eth.getBlockNumber();
            if (lastBn == currBn) {
                await new Promise((resolve, reject) => {
                    setTimeout(resolve, 500);
                });
                continue;
            }

            try {
                let events = await web3.eth.getPastLogs({
                    fromBlock: lastBn + 1,
                    toBlock: currBn,
                    address: address,
                    topics: [eventId],
                });

                for (let event of events) {
                    console.log("event", event);

                    let topics = event.topics;

                    let node = {
                        address: address,
                        name: eventName,
                        data: [],
                    };

                    for (let i = 1; i < topics.length; i++) {
                        node.data.push(topics[i]);
                    }

                    this.eventMgr.send(eventKey, node);
                }
            } catch (error) {
                console.error("listenLog", error.toString());
            }

            lastBn = currBn;
        }
    }

    cutInteger(integer, bits, shift) {
        let result = integer.shiftRight(shift)
            .and(BigInteger.ONE.shiftLeft(bits)
                .subtract(BigInteger.ONE));

        return Number(result.toString());
    }

    toReal(amount, decimal = 18) {
        let dec = BigDecimal.TEN.pow(new BigDecimal(decimal.toString()));
        return new BigDecimal(amount.toString()).multiply(dec)
        .setScale(0, BigDecimal.ROUND_DOWN).toString();
    }

    toDisplay(amount, decimal = 18, scale = 4) {
        let dec = BigDecimal.TEN.pow(new BigDecimal(decimal.toString()));
        return new BigDecimal(amount.toString())
        .divide(dec, Number(scale), BigDecimal.ROUND_DOWN).toString();
    }

    toBigInteger(n) {
        n = n.toString().toLowerCase();

        if (n.startsWith("0x")) {
            return new BigInteger(n.substring(2), 16);
        } else {
            return new BigInteger(n);
        }
    }

    toHex(num, width) {
        num = new BigInteger(num.toString()).toString(16);
        while (num.length < width) {
            num = "0" + num;
        }

        return num;
    }

    addressEquals(a, b) {
        return this.toBigInteger(a).compareTo(this.toBigInteger(b)) == 0;
    }

    async getETHBalance() {
        return await web3.eth.getBalance(this.account);
    }

    async getTokenBalance(address, account) {
        return await this.call({
            abi: ABIS.LP,
            address: address,
            method: "balanceOf",
            args: [account || this.account],
        });
    }

    async getTokenAllowance(address, spender) {
        return await this.call({
            abi: ABIS.LP,
            address: address,
            method: "allowance",
            args: [this.account, spender],
        });
    }

    async approveToken(address, spender, amount) {
        await this.send({
            abi: ABIS.LP,
            address: address,
            method: "approve",
            args: [spender, amount],
        });
    }

    async calcPrice(params) {
        let multiply = new BigDecimal((params.multiply || 1).toString());

        let result = {};

        let reserves = await this.call({
            abi: ABIS.LP,
            address: params.address,
            method: "getReserves",
        });

        if (params.swap) {
            let temp = reserves[0];
            reserves[0] = reserves[1];
            reserves[1] = temp;

            temp = params.decimal0;
            params.decimal0 = params.decimal1;
            params.decimal1 = temp;
        }

        result.tokenPrice = new BigDecimal(this.toDisplay(
                    reserves[0], params.decimal0, params.decimal0))
            .multiply(multiply)
            .divide(new BigDecimal(this.toDisplay(
                        reserves[1], params.decimal1, params.decimal)),
                8, BigDecimal.ROUND_DOWN)
            .toString();

        if (params.decimalLP) {
            let totalSupply = await this.call({
                abi: ABIS.LP,
                address: params.address,
                method: "totalSupply",
            });

            result.lpPrice = new BigDecimal(this.toDisplay(
                        reserves[0], params.decimal0, params.decimal0))
                .multiply(new BigDecimal(this.toReal(2, params.decimalLP)))
                .multiply(multiply)
                .divide(new BigDecimal(totalSupply.toString()), 8, BigDecimal.ROUND_DOWN)
                .toString();
        }

        return result;
    }

    async requestTokenPrice() {
        let result = {
            HUSD: "1",
            USDT: "1",
        };

        let response = await this.requestHttp({
            url: "//heroapi.juhuixinkj.com/api/watermargin/getCryptoCurrencyPricing",
        });

        for (let item of response.json.list) {
            result[item.name] = item.price;
        }

        result.WHT = result.HT;

        let prices;

        prices = await this.calcPrice({
            address: TKConfig.TOKEN.STK_USDT.address,
            decimal0: TKConfig.TOKEN.USDT.decimal,
            decimal1: TKConfig.TOKEN.STK.decimal,
            decimalLP: TKConfig.TOKEN.STK_USDT.decimal,
        });
        result.STK = prices.tokenPrice;
        result.STK_USDT = prices.lpPrice;

        prices = await this.calcPrice({
            address: TKConfig.TOKEN.SH_HUSD.address,
            decimal0: TKConfig.TOKEN.HUSD.decimal,
            decimal1: TKConfig.TOKEN.SH.decimal,
            decimalLP: TKConfig.TOKEN.SH_HUSD.decimal,
        });
        result.SH = prices.tokenPrice;
        result.SH_HUSD = prices.lpPrice;

        prices = await this.calcPrice({
            address: TKConfig.TOKEN.STK_SH.address,
            decimal0: TKConfig.TOKEN.SH.decimal,
            decimal1: TKConfig.TOKEN.STK.decimal,
            decimalLP: TKConfig.TOKEN.STK_SH.decimal,
            multiply: result.SH,
        });
        result.STK_SH = prices.lpPrice;

        prices = await this.calcPrice({
            address: TKConfig.TOKEN.CRM_USDT.address,
            decimal0: TKConfig.TOKEN.CRM.decimal,
            decimal1: TKConfig.TOKEN.USDT.decimal,
            swap: true,
        });
        result.CRM = prices.tokenPrice;

        return result;
    }

    async getCardTotalSupply(address) {
        return await this.call({
            abi: ABIS.Card,
            address: address,
            method: "totalSupply",
        });
    }

    async getCardBalance(address) {
        return await this.call({
            abi: ABIS.Card,
            address: address,
            method: "balanceOf",
            args: [this.account],
        });
    }

    async transferCards(address, to, ids) {
        await this.send({
            abi: ABIS.Card,
            address: address,
            method: "batchTransferFrom",
            args: [this.account, to, ids],
        });
    }

    async burnCards(address, ids) {
        await this.send({
            abi: ABIS.Card,
            address: address,
            method: "batchBurn",
            args: [ids],
        });
    }

    async getHeroPackages() {
        let ids = await this.call({
            abi: ABIS.Package,
            address: this.addrHeroPackage,
            method: "tokensOf",
            args: [this.account, 0, 0],
        });

        let packages = [];

        for (let id of ids) {
            packages.push(this.getHeroPackageInfo(id));
        }

        return packages;
    }

    getHeroPackageInfo(id) {
        id = this.toBigInteger(id);

        let info = {
            id: id.toString(),
            shop: this.cutInteger(id, 16, 240),
            tokenAmount: this.cutInteger(id, 72, 168),
            quantity: this.cutInteger(id, 16, 152),
            padding: this.cutInteger(id, 56, 96),
            mintTime: this.cutInteger(id, 40, 56),
            index: this.cutInteger(id, 56, 0),
        };

        return info;
    }

    async getHeroPackageRarityWeights(id) {
        let packageInfo = this.getHeroPackageInfo(id);

        let target;
        for (let name in TKConfig.HERO_SHOP) {
            let shop = TKConfig.HERO_SHOP[name];
            if (shop.id == packageInfo.shop) {
                target = shop.address;
                break;
            }
        }

        let weights = await this.call({
            abi: ABIS.HeroShopToken,
            address: target,
            method: "getRarityWeights",
            args: [id],
        });

        let result = [];
        let totalWeight = 0;

        for (let i = 0; i < weights.length; i++) {
            result[i] = Number(weights[i]);
            totalWeight += result[i];
        }

        for (let i = 0; i < result.length; i++) {
            result[i] /= totalWeight;
        }

        return result;
    }

    async getHeroPackageStarWeights(id) {
        let packageInfo = this.getHeroPackageInfo(id);

        let target;
        for (let name in TKConfig.HERO_SHOP) {
            let shop = TKConfig.HERO_SHOP[name];
            if (shop.id == packageInfo.shop) {
                target = shop.address;
                break;
            }
        }

        let weights = await this.call({
            abi: ABIS.HeroShopToken,
            address: target,
            method: "getStarWeights",
            args: [id],
        });

        let result = [];
        let totalWeight = 0;

        for (let i = 0; i < weights.length; i++) {
            result[i] = Number(weights[i]);
            totalWeight += result[i];
        }

        for (let i = 0; i < result.length; i++) {
            result[i] /= totalWeight;
        }

        return result;
    }

    async openHeroPackage(id, callback) {
        this.send({
            abi: ABIS.Package,
            address: this.addrHeroPackage,
            method: "open",
            args: [id],
        });

        let info = this.getHeroPackageInfo(id);
        let index = 0;

        await new Promise((resolve, reject) => {
            this.eventMgr.addListener({
                name: this.addrHero.toLowerCase() + "," + "Transfer",
                callback: (name, event) => {
                    let data = event.data;

                    if (!this.addressEquals(data[0], 0)) {
                        return;
                    }

                    if (!this.addressEquals(data[1], this.account)) {
                        return;
                    }

                    index++;

                    if (callback) {
                        callback(info.quantity, index, data[2]);
                    }

                    if (index >= info.quantity) {
                        resolve();
                        return true;
                    }
                },
            });
        });
    }

    async getHeroes() {
        let ids = await this.call({
            abi: ABIS.Card,
            address: this.addrHero,
            method: "tokensOf",
            args: [this.account, 0, 0],
        });

        let heroes = [];

        for (let id of ids) {
            heroes.push(this.getHeroInfo(id));
        }

        return heroes;
    }

    getHeroInfo(id) {
        id = this.toBigInteger(id);

        let info = {
            id: id.toString(),
            country: this.cutInteger(id, 8, 248),
            order: this.cutInteger(id, 24, 224),
            skin: this.cutInteger(id, 8, 216),
            star: this.cutInteger(id, 8, 208),
            tokenAmount: this.cutInteger(id, 72, 136),
            padding: this.cutInteger(id, 24, 112),
            shop: this.cutInteger(id, 16, 96),
            mintTime: this.cutInteger(id, 40, 56),
            index: this.cutInteger(id, 56, 0),
        };

        info.heroInfo = this.clone(TKConfig.HERO[info.country][info.order]);

        if (info.heroInfo) {
            info.fight = TKConfig.HERO_RARITY_BUFFER[info.heroInfo.rarity]
                 * TKConfig.HERO_STAR_BUFFER[info.star];
        }

        info.exp = TKConfig.HERO_EXP[info.star];

        return info;
    }

    getHeroInfoByNull(country, order) {
        let id = new BigInteger(country.toString()).shiftLeft(248).add(
                new BigInteger(order.toString()).shiftLeft(224));

        return this.getHeroInfo(id.toString());
    }

    async getEquipPackages() {
        let ids = await this.call({
            abi: ABIS.Package,
            address: this.addrEquipPackage,
            method: "tokensOf",
            args: [this.account, 0, 0],
        });

        let packages = [];

        for (let id of ids) {
            packages.push(this.getEquipPackageInfo(id));
        }

        return packages;
    }

    getEquipPackageInfo(id) {
        id = this.toBigInteger(id);

        let info = {
            id: id.toString(),
            shop: this.cutInteger(id, 16, 240),
            quantity: this.cutInteger(id, 16, 224),
            padding: this.cutInteger(id, 128, 96),
            mintTime: this.cutInteger(id, 40, 56),
            index: this.cutInteger(id, 56, 0),
        };

        return info;
    }

    async getEquipPackageRarityWeights(id) {
        let packageInfo = this.getEquipPackageInfo(id);

        let target;
        for (let name in TKConfig.EQUIP_SHOP) {
            let shop = TKConfig.EQUIP_SHOP[name];
            if (shop.id == packageInfo.shop) {
                target = shop.address;
                break;
            }
        }

        let weights = await this.call({
            abi: ABIS.HeroShopToken,
            address: target,
            method: "getRarityWeights",
            args: [id],
        });

        let result = [];
        let totalWeight = 0;

        for (let i = 0; i < weights.length; i++) {
            result[i] = Number(weights[i]);
            totalWeight += result[i];
        }

        for (let i = 0; i < result.length; i++) {
            result[i] /= totalWeight;
        }

        return result;
    }

    async getEquipPackageStarWeights(id) {
        let packageInfo = this.getEquipPackageInfo(id);

        let target;
        for (let name in TKConfig.EQUIP_SHOP) {
            let shop = TKConfig.EQUIP_SHOP[name];
            if (shop.id == packageInfo.shop) {
                target = shop.address;
                break;
            }
        }

        let weights = await this.call({
            abi: ABIS.HeroShopToken,
            address: target,
            method: "getStarWeights",
            args: [id],
        });

        let result = [];
        let totalWeight = 0;

        for (let i = 0; i < weights.length; i++) {
            result[i] = Number(weights[i]);
            totalWeight += result[i];
        }

        for (let i = 0; i < result.length; i++) {
            result[i] /= totalWeight;
        }

        return result;
    }

    async openEquipPackage(id, callback) {
        this.send({
            abi: ABIS.Package,
            address: this.addrEquipPackage,
            method: "open",
            args: [id],
        });

        let info = this.getEquipPackageInfo(id);
        let index = 0;

        await new Promise((resolve, reject) => {
            this.eventMgr.addListener({
                name: this.addrEquip.toLowerCase() + "," + "Transfer",
                callback: (name, event) => {
                    let data = event.data;

                    if (!this.addressEquals(data[0], 0)) {
                        return;
                    }

                    if (!this.addressEquals(data[1], this.account)) {
                        return;
                    }

                    index++;

                    if (callback) {
                        callback(info.quantity, index, data[2]);
                    }

                    if (index >= info.quantity) {
                        resolve();
                        return true;
                    }
                },
            });
        });
    }

    async getEquips() {
        let ids = await this.call({
            abi: ABIS.Card,
            address: this.addrEquip,
            method: "tokensOf",
            args: [this.account, 0, 0],
        });

        let equips = [];

        for (let id of ids) {
            equips.push(this.getEquipInfo(id));
        }

        return equips;
    }

    getEquipInfo(id, hero) {
        id = this.toBigInteger(id);

        let info = {
            id: id.toString(),
            order: this.cutInteger(id, 24, 232),
            skin: this.cutInteger(id, 8, 224),
            star: this.cutInteger(id, 8, 216),
            padding: this.cutInteger(id, 104, 112),
            shop: this.cutInteger(id, 16, 96),
            mintTime: this.cutInteger(id, 40, 56),
            index: this.cutInteger(id, 56, 0),
        };

        info.equipInfo = this.clone(TKConfig.EQUIP[info.order]);

        info.buffer = TKConfig.EQUIP_RARITY_BUFFER[info.equipInfo.rarity]
             * TKConfig.EQUIP_STAR_BUFFER[info.star];

        info.specialBuffer = info.equipInfo.buffer * TKConfig.EQUIP_STAR_BUFFER[info.star];
        info.specialEnabled = this.checkEquipSpecial(hero, info);

        return info;
    }

    getEquipInfoByNull(order) {
        let id = new BigInteger(order.toString()).shiftLeft(232);

        return this.getEquipInfo(id.toString());
    }

    checkEquipSpecial(hero, equip) {
        if (!hero || !equip) {
            return false;
        }

        let heroInfo = hero.heroInfo;
        let equipInfo = equip.equipInfo;

        if (equipInfo.type != 0) {
            if (heroInfo.heroInfo.type != equipInfo.type) {
                return false;
            }
        }

        if (equipInfo.country != 0) {
            if (heroInfo.country != equipInfo.country) {
                return false;
            }
        }

        if (equipInfo.star != 0) {
            if (heroInfo.star < equipInfo.star) {
                return false;
            }
        }

        if (equipInfo.order != 0) {
            if (heroInfo.order != equipInfo.order) {
                return false;
            }
        }

        return true;
    }

    async getSlotInfo() {
        let data = await this.call({
            abi: ABIS.Slot,
            address: this.addrSlot,
            method: "getUserInfo",
            args: [this.account],
        });

        let result = {
            heroCount: Number(data.heroCount),
            countries: [],

            country: Number(data.country),
            countryChangeCount: Number(data.countryChangeCount),

            fight: Number(data.fight),
        };

        for (let i = 0; i < data.countries.length; i++) {
            let ct = data.countries[i];
            let country = {
                heroes: [],
                heroCount: Number(ct.heroCount),

                baseFight: Number(ct.baseFight),
                actualFight: Number(ct.actualFight),
            };
            result.countries.push(country);

            for (let j = 0; j < ct.heroes.length; j++) {
                let hr = ct.heroes[j];
                let hero = {
                    heroCardId: hr.heroCardId,

                    levelCeil: Number(hr.levelCeil),
                    levelActive: Number(hr.levelActive),
                    exp: Number(hr.exp),

                    locked: hr.locked,
                    actualFight: Number(hr.actualFight),
                };
                hero.level = hero.levelCeil;
                country.heroes.push(hero);

                hero.baseFight = 0;
                hero.bufferedFight = 0;
                hero.equiplessFight = 0;

                if (hero.heroCardId == "0") {
                    hero.heroInfo = this.getHeroInfoByNull(i + 1, j + 1);

                    if (!hero.heroInfo.heroInfo ||
                        hero.heroInfo.heroInfo.close ||
                        !hero.heroInfo.heroInfo.name) {

                        country.heroes.pop();
                    }
                } else {
                    hero.heroInfo = this.getHeroInfo(hero.heroCardId);

                    hero.baseFight = TKConfig.HERO_RARITY_BUFFER[hero.heroInfo.heroInfo.rarity]
                         * TKConfig.HERO_STAR_BUFFER[hero.heroInfo.star];

                    hero.bufferedFight = Math.floor(hero.baseFight *
                            (1 + TKConfig.HERO_SLOT_LEVEL[hero.levelActive].buffer));

                    if (i + 1 == result.country) {
                        hero.equiplessFight = Math.floor(hero.bufferedFight * 1.5);
                    } else if (i + 1 == TKConfig.COUNTRY_QUN) {
                        hero.equiplessFight = Math.floor(hero.bufferedFight * 1.2);
                    } else {
                        hero.equiplessFight = hero.bufferedFight;
                    }
                }

                if (hr.equipCardId != "0") {
                    hero.equip = this.getEquipInfo(hr.equipCardId, hero);
                }
            }
        }

        let heroes = await this.getHeroes();

        for (let hero of heroes) {
            let slots = result.countries[hero.country - 1].heroes;
            let slot;

            for (let temp of slots) {
                if (hero.order == temp.heroInfo.order) {
                    slot = temp;
                    break;
                }
            }

            if (!slot) {
                continue;
            }

            if (slot.highCard) {
                if (slot.highCard.star < hero.star) {
                    slot.highCard = hero;
                }
            } else {
                if (slot.heroCardId == "0" || slot.heroInfo.star < hero.star) {
                    slot.highCard = hero;
                }
            }
        }

        return result;
    }

    async changeSlotCountry(country) {
        await this.send({
            abi: ABIS.Slot,
            address: this.addrSlot,
            method: "changeCountry",
            args: [country],
        });
    }

    async addHeroToSlot(ids) {
        await this.send({
            abi: ABIS.Card,
            address: this.addrHero,
            method: "safeBatchTransferFrom",
            args: [this.account, this.addrSlot, ids, "0x01"],
        });
    }

    async addHighHeroToSlot(limit = 10000) {
        let info = await this.getSlotInfo();
        let ids = [];

        for (let country of info.countries) {
            for (let hero of country.heroes) {
                if (hero.highCard) {
                    ids.push(hero.highCard.id);

                    if (ids.length >= limit) {
                        break;
                    }
                }
            }
        }

        if (ids.length <= 0) {
            throw "no high card";
        }

        await this.addHeroToSlot(ids);
    }

    async removeHeroFromSlot(heroes) {
        let hs = [];

        for (let i = 0; i < heroes.length; i++) {
            let hero = heroes[i];
            hs.push((hero.country << 24) | hero.order);
        }

        await this.send({
            abi: ABIS.Slot,
            address: this.addrSlot,
            method: "removeHeroes",
            args: [hs],
        });
    }

    async removeHeroFromSlotAll() {
        await this.send({
            abi: ABIS.Slot,
            address: this.addrSlot,
            method: "removeHeroesAll",
        });
    }

    async addEquipToSlot(ids, targets) {
        let data = "0x01";

        for (let target of targets) {
            data += this.toHex(target.country, 2) + this.toHex(target.order, 6);
        }

        await this.send({
            abi: ABIS.Card,
            address: this.addrEquip,
            method: "safeBatchTransferFrom",
            args: [this.account, this.addrSlot, ids, data],
        });
    }

    async moveEquipOnSlot(items) {
        let ids = [];
        let data = "0x02";

        for (let item of items) {
            if (!item.from) {
                ids.push(item.equip.id);

                let to = item.to.heroInfo;
                data += this.toHex(to.country, 2) + this.toHex(to.order, 6);
            }
        }

        for (let item of items) {
            if (item.from) {
                data += this.toHex(item.equip.id, 64);

                let from = item.from.heroInfo;
                data += this.toHex(from.country, 2) + this.toHex(from.order, 6);

                let to = item.to.heroInfo;
                data += this.toHex(to.country, 2) + this.toHex(to.order, 6);
            }
        }

        await this.send({
            abi: ABIS.Card,
            address: this.addrEquip,
            method: "safeBatchTransferFrom",
            args: [this.account, this.addrSlot, ids, data],
        });
    }

    async removeEquipFromSlot(targets) {
        let ts = [];

        for (let target of targets) {
            ts.push((target.country << 24) | target.order);
        }

        await this.send({
            abi: ABIS.Slot,
            address: this.addrSlot,
            method: "removeEquips",
            args: [ts],
        });
    }

    checkUpgradeSlot(slotInfo, heroIds) {
        slotInfo = this.clone(slotInfo);

        let levelMax = TKConfig.HERO_SLOT_LEVEL.length - 1;

        for (let id of heroIds) {
            let hero = this.getHeroInfo(id);
            let slot;

            for (let i = 0; ; i++) {
                let temp = slotInfo.countries[hero.country - 1].heroes[i];
                if (!temp) {
                    throw "not found";
                }

                if (temp.heroInfo.order == hero.order) {
                    slot = temp;
                    break;
                }
            }

            if (slot.levelCeil >= levelMax) {
                return null;
            }

            slot.exp += TKConfig.HERO_EXP[hero.star];
            while (slot.levelCeil < levelMax) {
                let cost = TKConfig.HERO_SLOT_LEVEL[slot.levelCeil].exp;
                if (slot.exp < cost) {
                    break;
                } else {
                    slot.exp -= cost;
                    slot.levelCeil++;
                    slot.level++;
                }
            }
        }

        return slotInfo;
    }

    async upgradeSlot(ids) {
        await this.send({
            abi: ABIS.Card,
            address: this.addrHero,
            method: "safeBatchTransferFrom",
            args: [this.account, this.addrSlot, ids, "0x02"],
        });
    }

    async stakeSlot(params, useLocked) {
        await this.send({
            abi: ABIS.Slot,
            address: this.addrSlot,
            method: "stakeHeroLevels",
            args: [params, useLocked],
        });
    }

    async redeemSlot(params) {
        await this.send({
            abi: ABIS.Slot,
            address: this.addrSlot,
            method: "redeemHeroLevels",
            args: [params],
        });
    }

    async getSlotRelease() {
        let data = await this.call({
            abi: ABIS.Slot,
            address: this.addrSlot,
            method: "linearReleases",
            args: [this.account],
        });

        return {
            released: data.released,
            locked: data.locked,
            startTime: Number(data.startTime),
            duration: Number(data.duration),
        };
    }

    calcSlotRelease(data) {
        let result = {
            startTime: data.startTime,
            endTime: data.startTime + data.duration,
        };

        let duration = this.currSecond() - data.startTime;
        duration = Math.min(duration, data.duration);

        let released = new BigInteger(data.locked.toString())
            .multiply(new BigInteger(duration.toString()))
            .divide(new BigInteger(data.duration.toString()));

        result.locked = new BigInteger(data.locked.toString())
            .subtract(released).toString();

        result.released = new BigInteger(data.released.toString())
            .add(released).toString();

        return result;
    }

    async withdrawSlotRelease() {
        await this.send({
            abi: ABIS.Slot,
            address: this.addrSlot,
            method: "withdrawRelease",
        });
    }

    async getMineralMultiInfo(address, index) {
        let data = await this.call({
            abi: ABIS.Slot,
            address: address,
            method: "getMineralInfo",
            args: [index, this.account],
        });

        let result = {
            totalRate: data.rate,
            totalAmount: data.amount,
            updateTime: Number(data.updateTime),

            fullReward: data.reward,
            startTime: Number(data.startTime),
            duration: Number(data.duration),

            rate: data.user.rate,
            amount: data.user.amount,
            reward: data.user.reward,
        };

        return result;
    }

    calcSlotMine(info, amount) {
        let result = this.clone(info);
        result.endTime = result.startTime + result.duration;

        let now = this.currSecond();

        let duration;
        if (now <= result.startTime) {
            duration = 0;
        } else {
            duration = Math.min(now - result.startTime, result.duration);
        }

        result.totalReward = new BigInteger(result.fullReward.toString())
            .multiply(new BigInteger(duration.toString()))
            .divide(new BigInteger(result.duration.toString()))
            .toString();

        if (!amount) {
            amount = result.amount;
        }

        if (amount == 0 ||
            now <= result.startTime ||
            result.updateTime >= result.endTime) {

            result.rewardPerDay = "0";
        } else {
            let start = Math.max(result.startTime, result.updateTime);
            let end = Math.min(result.endTime, now);
            let deno = new BigInteger("10").pow(18);

            let reward = new BigInteger((end - start).toString())
                .multiply(new BigInteger(result.fullReward.toString()))
                .divide(new BigInteger(result.duration.toString()));
            let rate = reward.multiply(deno)
                .divide(new BigInteger(result.totalAmount.toString()))
                .add(new BigInteger(result.totalRate.toString()));

            result.reward = rate.subtract(new BigInteger(result.rate.toString()))
                .multiply(new BigInteger(amount.toString()))
                .divide(deno)
                .add(new BigInteger(result.reward.toString()));

            result.rewardPerDay = new BigInteger(result.fullReward.toString())
                .multiply(new BigInteger(amount.toString()))
                .multiply(new BigInteger("86400"))
                .divide(new BigInteger(result.totalAmount.toString()))
                .divide(new BigInteger(result.duration.toString()))
                .toString();
        }

        return result;
    }

    async calcCountryChangedData(country) {
        let slotInfo = await this.getSlotInfo();
        let fight = 0;

        for (let c = 1; c <= 4; ++c) {
            let countryFight = slotInfo.countries[c - 1].baseFight;

            if (c == TKConfig.COUNTRY_QUN) {
                countryFight = Math.floor(countryFight * 1.2);
            } else if (c == country) {
                countryFight = Math.floor(countryFight * 1.5);
            }

            fight += countryFight;
        }

        let countBuffer = Math.floor(slotInfo.heroCount / 8) * 0.05;
        fight = Math.floor(fight * (1 + countBuffer));

        let result = {
            fight: fight,
        };

        let data = await this.getMineralMultiInfo(this.addrSlot, country);

        let totalAmount = new BigInteger(data.totalAmount.toString())
            .add(new BigInteger(result.fight.toString()));

        if (totalAmount.compareTo(BigInteger.ZERO) == 0) {
            result.reward = "0";
        } else {
            result.reward = new BigInteger(data.fullReward.toString())
                .multiply(new BigInteger(result.fight.toString()))
                .multiply(new BigInteger("86400"))
                .divide(totalAmount)
                .divide(new BigInteger(data.duration.toString()))
                .toString();
        }

        return result;
    }

    async getMineralRewards(address) {
        return await this.call({
            abi: ABIS.Slot,
            address: address,
            method: "calcMineralRewards",
            args: [[1, 2, 3], this.account],
        });
    }

    async withdrawSlotMineAll() {
        await this.send({
            abi: ABIS.Slot,
            address: this.addrSlot,
            method: "withdrawRewardAll",
        });
    }

    async requestMarketRetails(address, params) {
        let contract = this.getContract(ABIS.Market2, address);

        if (!params) {
            params = {};
        }

        let retails = [];
        let map = {};
        let batch = 1000;

        for (let i = 0; ; i += batch) {
            let result = await this.call({
                contract: contract,
                method: "getRetails",
                args: [i, 0, batch,
                    params.ownerAddress || this.ADDRESS_ZERO,
                    params.nftAddress || this.ADDRESS_ZERO,
                    params.moneyAddress || this.ADDRESS_ZERO],
            });

            for (let j = 0; ; j++) {
                let retail = result[j];
                if (!retail) {
                    break;
                }

                if (retail.id == 0) {
                    return retails;
                }

                if (map[retail.id]) {
                    continue;
                }
                map[retail.id] = true;

                let item = this.clone(retail);
                retails.push(item);

                let nftId = item.nftId || item.nftIds[0];

                if (this.addressEquals(item.nft, this.addrHero)) {
                    item.heroInfo = this.getHeroInfo(nftId);
                } else if (this.addressEquals(item.nft, this.addrEquip)) {
                    item.equipInfo = this.getEquipInfo(nftId);
                }
            }
        }

        return retails;
    }

    async addMarketRetail(marketAddress, nftAddress, nftId, money, price) {
        let data = "0x01";

        money = this.toBigInteger(money).toString(16);
        while (money.length < 64) {
            money = "0" + money;
        }
        data += money;

        price = this.toBigInteger(price).toString(16);
        while (price.length < 64) {
            price = "0" + price;
        }
        data += price;

        await this.send({
            abi: ABIS.Card,
            address: nftAddress,
            method: "safeTransferFrom",
            args: [this.account, marketAddress, nftId, data],
        });
    }

    async addMarketRetails(marketAddress, nftAddress, nftIds, money, price) {
        let data = "0x01";

        money = this.toBigInteger(money).toString(16);
        while (money.length < 64) {
            money = "0" + money;
        }
        data += money;

        price = this.toBigInteger(price).toString(16);
        while (price.length < 64) {
            price = "0" + price;
        }
        data += price;

        await this.send({
            abi: ABIS.Card,
            address: nftAddress,
            method: "safeBatchTransferFrom",
            args: [this.account, marketAddress, nftIds, data],
        });
    }

    async removeMarketRetail(address, id) {
        await this.send({
            abi: ABIS.Market2,
            address: address,
            method: "removeRetail",
            args: [id],
        });
    }

    async removeMarketRetails(address, ids) {
        await this.send({
            abi: ABIS.Market2,
            address: address,
            method: "removeRetails",
            args: [ids],
        });
    }

    async buyMarketRetail(address, id, moneyIn, amountInMax) {
        let value = 0;
        if (moneyIn == TKConfig.TOKEN.HT.address) {
            value = amountInMax;
        }

        let deadline = this.currSecond() + 60;

        await this.send({
            abi: ABIS.Market2,
            address: address,
            method: "buy",
            args: [id, moneyIn, amountInMax, deadline],
            value: value,
            gas: 3000000,
        });
    }

    async requestMarketBalance(address, moneyTo) {
        return await this.call({
            abi: ABIS.Market2,
            address: address,
            method: "balances",
            args: [this.account, moneyTo],
        });
    }

    async withdrawMarket(address, moneyTo) {
        await this.send({
            abi: ABIS.Market2,
            address: address,
            method: "withdraw",
            args: [moneyTo],
        });
    }

    async requestMarketMoneyWhite(address, moneyTo) {
        let data = await this.call({
            abi: ABIS.Market2,
            address: address,
            method: "getMoneyWhite",
            args: [moneyTo],
        });

        let swaps = data[3];
        let result = [];

        for (let i = 0; i < swaps.length; i++) {
            let swap = swaps[i];
            let token = this.getTokenByAddress(swap.path[0]);

            if (token) {
                result.push({
                    router: swap.router,
                    path: swap.path,
                    token: token,
                });
            }
        }

        return result;
    }

    async calcSwapExactTokenForToken(params) {
        let result = {};

        if (params.router) {
            let path = [];
            for (let i = 0; i < params.path.length; i++) {
                if (params.path[i] == TKConfig.TOKEN.HT.address) {
                    path.push(TKConfig.TOKEN.WHT.address);
                } else {
                    path.push(params.path[i]);
                }
            }

            let amountsOut = await this.call({
                abi: ABIS.ISwapRouter,
                address: params.router,
                method: "getAmountsOut",
                args: [params.amountIn, path],
            });

            result.amountOut = amountsOut[amountsOut.length - 1].toString();
        } else {
            let reserves = await this.call({
                contract: params.pair,
                method: "getReserves",
            });

            let tokenIn = await this.call({
                contract: params.pair,
                method: "token0",
            });

            let reserveIn;
            let reserveOut;
            if (this.addressEquals(tokenIn, params.tokenIn)) {
                reserveIn = reserves[0];
                reserveOut = reserves[1];
            } else {
                reserveIn = reserves[1];
                reserveOut = reserves[0];
            }
            reserveIn = new BigDecimal(reserveIn.toString());
            reserveOut = new BigDecimal(reserveOut.toString());

            let amountIn = new BigDecimal(params.amountIn.toString());
            let ao = amountIn.multiply(reserveOut)
                .divide(reserveIn, 0, BigDecimal.ROUND_FLOOR);
            if (ao.compareTo(BigDecimal.ZERO) == 0) {
                throw "invalid input";
            }

            let fee = amountIn
                .multiply(new BigDecimal(params.feeRate.toString()))
                .setScale(0, BigDecimal.ROUND_CEILING);

            let amountOut = reserveIn.multiply(reserveOut)
                .divide(reserveIn.add(amountIn).subtract(fee),
                    0, BigDecimal.ROUND_CEILING);
            amountOut = reserveOut.subtract(amountOut);

            let slip = amountIn.subtract(fee).multiply(reserveOut)
                .divide(reserveIn, 0, BigDecimal.ROUND_FLOOR)
                .subtract(amountOut)
                .divide(amountIn.multiply(reserveOut)
                    .divide(reserveIn, 0, BigDecimal.ROUND_FLOOR),
                    4, BigDecimal.ROUND_FLOOR);

            result.reserveIn = reserveIn.toString();
            result.reserveOut = reserveOut.toString();
            result.amountOut = amountOut.toString();
            result.fee = fee.toString();
            result.slip = slip.toString();
        }

        let amountOutMin = new BigDecimal(result.amountOut.toString()).multiply(
                new BigDecimal((1 - Number(params.slip)).toString()))
            .setScale(0, BigDecimal.ROUND_FLOOR);

        result.amountOutMin = amountOutMin.toString();

        return result;
    }

    async calcSwapTokenForExactToken(params) {
        let result = {};

        if (params.router) {
            let path = [];
            for (let i = 0; i < params.path.length; i++) {
                if (params.path[i] == TKConfig.TOKEN.HT.address) {
                    path.push(TKConfig.TOKEN.WHT.address);
                } else {
                    path.push(params.path[i]);
                }
            }

            let amountsIn = await this.call({
                abi: ABIS.ISwapRouter,
                address: params.router,
                method: "getAmountsIn",
                args: [params.amountOut, path],
            });

            result.amountIn = amountsIn[0].toString();
        } else {
            let reserves = await this.call({
                contract: params.pair,
                method: "getReserves",
            });

            let tokenIn = await this.call({
                contract: params.pair,
                method: "token0",
            });

            let reserveIn;
            let reserveOut;
            if (this.addressEquals(tokenIn, params.tokenIn)) {
                reserveIn = reserves[0];
                reserveOut = reserves[1];
            } else {
                reserveIn = reserves[1];
                reserveOut = reserves[0];
            }
            reserveIn = new BigDecimal(reserveIn.toString());
            reserveOut = new BigDecimal(reserveOut.toString());

            let amountOut = new BigDecimal(params.amountOut.toString());
            let left = reserveOut.subtract(amountOut);
            if (left.compareTo(BigDecimal.ZERO) <= 0) {
                throw "invalid input";
            }

            let amountIn = reserveIn.multiply(reserveOut)
                .divide(left, 0, BigDecimal.ROUND_CEILING);
            amountIn = amountIn.subtract(reserveIn);

            let feeRate = new BigDecimal(params.feeRate.toString());
            let fee = amountIn.multiply(feeRate)
                .divide(BigDecimal.ONE.subtract(feeRate), 0, BigDecimal.ROUND_CEILING);

            let slip = amountIn.subtract(fee).multiply(reserveOut)
                .divide(reserveIn, 0, BigDecimal.ROUND_FLOOR)
                .subtract(amountOut)
                .divide(amountIn.multiply(reserveOut)
                    .divide(reserveIn, 0, BigDecimal.ROUND_FLOOR),
                    4, BigDecimal.ROUND_FLOOR);

            result.reserveIn = reserveIn.toString();
            result.reserveOut = reserveOut.toString();
            result.amountIn = amountIn.toString();
            result.fee = fee.toString();
            result.slip = slip.toString();
        }

        let amountInMax = new BigDecimal(result.amountIn).multiply(
                new BigDecimal((1 + Number(params.slip)).toString()))
            .setScale(0, BigDecimal.ROUND_FLOOR);

        result.amountInMax = amountInMax.toString();

        return result;
    }

    async buyShopQuantity(address, quantity) {
        await this.send({
            abi: ABIS.HeroShopMoney,
            address: address,
            method: "buy",
            args: [quantity],
        });
    }

    async buyShopTokenAmountQuantity(address, tokenAmount, quantity) {
        await this.send({
            abi: ABIS.HeroShopToken,
            address: address,
            method: "buy",
            args: [tokenAmount, quantity],
        });
    }

    async getShopQuantityLimit(address) {
        let data = await this.call({
            abi: ABIS.HeroShopToken,
            address: address,
            method: "getQuantitiyLimit",
        });

        return {
            totalMax: data[0],
            userMax: data[1],
            singleMin: data[2],
            singleMax: data[3],
        };
    }

    async getShopQuantityTotal(address) {
        return await this.call({
            abi: ABIS.HeroShopToken,
            address: address,
            method: "quantityTotal",
        });
    }

    async getShopQuantityUser(address) {
        return await this.call({
            abi: ABIS.HeroShopToken,
            address: address,
            method: "quantityUsers",
            args: [this.account],
        });
    }

    async getFixedStartTime(address) {
        return await this.call({
            abi: ABIS.HeroShopToken,
            address: address,
            method: "fixedStartTime",
        });
    }

    async getFixedEndTime(address) {
        return await this.call({
            abi: ABIS.HeroShopToken,
            address: address,
            method: "fixedEndTime",
        });
    }

    async getShopPrice(address) {
        return await this.call({
            abi: ABIS.HeroShopMoney,
            address: address,
            method: "price",
        });
    }

    async getShopTokenAmount(address) {
        return await this.call({
            abi: ABIS.HeroShopMoney,
            address: address,
            method: "tokenAmount",
        });
    }

    async getMineralSingleInfo(address) {
        return await this.call({
            abi: ABIS.HeroShopMoneyMineral,
            address: address,
            method: "getMineralInfo",
            args: [this.account],
        });
    }

    async getMineralSingleReward(address) {
        return await this.call({
            abi: ABIS.HeroShopMoneyMineral,
            address: address,
            method: "calcMineralReward",
            args: [this.account],
        });
    }

    async addMineralSingleAmount(address, amount, value = 0) {
        await this.send({
            abi: ABIS.HeroShopMoneyMineral,
            address: address,
            method: "addMineralAmount",
            args: [amount],
            value: value,
            gas: 3000000,
        });
    }

    async subMineralSingleAmount(address, amount) {
        await this.send({
            abi: ABIS.HeroShopMoneyMineral,
            address: address,
            method: "subMineralAmount",
            args: [amount],
            gas: 8000000,
        });
    }

    async getWaterMarginFight() {
        return await this.call({
            abi: ABIS.IWaterMarginCardMine,
            address: this.addrWaterMarginCardMine,
            method: "mortgageAmounts",
            args: [this.account],
        });
    }

    async getDrawLotsInfo(address) {
        let data = await this.call({
            abi: ABIS.HeroShopDrawLots,
            address: address,
            method: "getLotInfo",
            args: [this.account],
        });

        return {
            buyStartTime: data[0],
            drawStartTime: data[1],
            pickStartTime: data[2],
            quantityMax: data[3],
            lotNumber: data[4],
            user: data[5],
        };
    }

    async getDrawLotsLucks(address) {
        return await this.call({
            abi: ABIS.HeroShopDrawLots,
            address: address,
            method: "getLucks",
            args: [0, 0],
        });
    }

    async getDrawLotsLucks2(address) {
        return await this.call({
            abi: ABIS.DrawLotsMoneyBack,
            address: address,
            method: "getLucks",
            args: [0, 0],
        });
    }

    async buyDrawLots(address, quantity) {
        await this.send({
            abi: ABIS.HeroShopDrawLots,
            address: address,
            method: "buyLot",
            args: [quantity],
        });
    }

    async pickDrawLotsCost(address) {
        await this.send({
            abi: ABIS.HeroShopDrawLots,
            address: address,
            method: "pickLotCost",
        });
    }

    async pickDrawLotsLuck(address) {
        await this.send({
            abi: ABIS.HeroShopDrawLots,
            address: address,
            method: "pickLotLuck",
        });
    }

    async getScoreOfScoreBoard(name) {
        return await this.call({
            abi: ABIS.ScoreBoard,
            address: this.addrScoreBoard,
            method: "userScores",
            args: [name, this.account],
        });
    }

    async getScoreValue(address) {
        return await this.call({
            abi: ABIS.MoneyToScore,
            address: address,
            method: "scoreValue",
        });
    }

    async getBuyCount(address) {
        let day = Math.floor((this.currSecond() + 60 * 60 * 8) / (60 * 60 * 24));

        return await this.call({
            abi: ABIS.MoneyToScore,
            address: address,
            method: "buyCounts",
            args: [day, this.account],
        });
    }

    async getBuyLimit(address) {
        let baseInfo = await this.call({
            abi: ABIS.Slot,
            address: this.addrSlot,
            method: "getUserBaseInfo",
            args: [this.account],
        });

        let fight = baseInfo[3];

        let limits = await this.call({
            abi: ABIS.MoneyToScore,
            address: address,
            method: "getBuyLimits",
        });

        let max = 0;

        for (let i = 0; i < limits.length; i++) {
            let limit = limits[i];

            if (fight >= limit.value) {
                max = limit.quantity;
            } else {
                break;
            }
        }

        return {
            fight: fight,
            max: max,
        };
    }

    async pickReward(address) {
        await this.send({
            abi: ABIS.CompeteScore,
            address: address,
            method: "pickReward",
        });
    }

    async safeBatchTransferFrom(nft, to, ids, data) {
        await this.send({
            abi: ABIS.Card,
            address: nft,
            method: "safeBatchTransferFrom",
            args: [this.account, to, ids, data],
        });
    }

    async getEquipPackageStarWeights2(id) {
        let packageInfo = this.getEquipPackageInfo(id);

        let target;
        for (let name in TKConfig.EQUIP_SHOP) {
            let shop = TKConfig.EQUIP_SHOP[name];
            if (shop.id == packageInfo.shop) {
                target = shop.address;
                break;
            }
        }

        let weights = await this.call({
            abi: ABIS.EquipShopMerge,
            address: target,
            method: "getStarWeights2",
            args: [id],
        });

        let result = [];
        let totalWeight = 0;

        for (let i = 0; i < weights.length; i++) {
            result[i] = Number(weights[i]);
            totalWeight += result[i];
        }

        for (let i = 0; i < result.length; i++) {
            result[i] /= totalWeight;
        }

        return result;
    }

    async modifyNickname(nickname) {
        await this.send({
            abi: ABIS.Nickname,
            address: this.addrNickname,
            method: "modify",
            args: [nickname],
        });
    }

    async getNickname(account) {
        return await this.call({
            abi: ABIS.Nickname,
            address: this.addrNickname,
            method: "nicknames",
            args: [account],
        });
    }

    async getMoneyInfo(address) {
        return await this.call({
            abi: ABIS.MoneyExchange,
            address: address,
            method: "getMoneyInfo",
            args: [this.account],
        });
    }

    async depositExchange(address, amount) {
        await this.send({
            abi: ABIS.MoneyExchange,
            address: address,
            method: "deposit",
            args: [amount],
        });
    }

    async withdrawExchange(address) {
        await this.send({
            abi: ABIS.MoneyExchange,
            address: address,
            method: "withdraw",
        });
    }

    _setLanguage(nodes, language) {
        for (let index in nodes) {
            let node = nodes[index];

            for (let key in node) {
                if (key.endsWith(language)) {
                    node[key.substring(0, key.length - language.length)] = node[key];
                }
            }
        }
    }

    setLanguage(language) {
        for (let country = 1; country <= 4; country++) {
            this._setLanguage(TKConfig.HERO[country], language);
        }

        this._setLanguage(TKConfig.EQUIP, language);
    }

    async getAmountMin(address) {
        return await this.call({
            abi: ABIS.CrossChainMoney,
            address: address,
            method: "amountMin",
        });
    }

    async getAmountMax(address) {
        return await this.call({
            abi: ABIS.CrossChainMoney,
            address: address,
            method: "amountMax",
        });
    }

    async getOtherBalances(address, chain) {
        return await this.call({
            abi: ABIS.CrossChainMoney,
            address: address,
            method: "otherBalances",
            args: [chain],
        });
    }

    async crossChainMoney(address, user, chain, amount) {
        await this.send({
            abi: ABIS.CrossChainMoney,
            address: address,
            method: "cross",
            args: [user, chain, amount],
        });
    }

    km(weights) {
        let size = weights.length;

        let weghtA = new Array(size);
        let weghtB = new Array(size);

        let matchA = new Array(size);
        let matchB = new Array(size);

        let result = new Array(size);

        for (let i = 0; i < size; i++) {
            let max = -1 / 0;

            for (let j = 0; j < size; j++) {
                max = Math.max(max, weights[i][j]);
            }

            weghtA[i] = max;
            weghtB[i] = 0;

            matchA[i] = false;
            matchB[i] = false;

            result[i] = -1;
        }

        let equals = (a, b) => {
            return Math.abs(a - b) < 1e-8;
        };

        let find = (a) => {
            if (matchA[a]) {
                return false;
            }
            matchA[a] = true;

            for (let i = 0; i < size; i++) {
                if (matchB[i] == false && equals(weghtA[a] + weghtB[i], weights[a][i])) {
                    matchB[i] = true;

                    let rst = result[i];
                    if (rst == -1 || find(rst)) {
                        result[i] = a;
                        return true;
                    }
                }
            }

            return false;
        };

        for (let i = 0; i < size; i++) {
            while (true) {
                for (let j = 0; j < size; j++) {
                    matchA[j] = false;
                    matchB[j] = false;
                }

                if (find(i)) {
                    break;
                }

                let min = 1 / 0;

                for (let j = 0; j < size; j++) {
                    if (matchA[j] == false) {
                        continue;
                    }

                    for (let k = 0; k < size; k++) {
                        if (matchB[k]) {
                            continue;
                        }

                        let delta = weghtA[j] + weghtB[k] - weights[j][k];
                        min = Math.min(min, delta);
                    }
                }

                for (let j = 0; j < size; j++) {
                    if (matchA[j]) {
                        weghtA[j] -= min;
                    }

                    if (matchB[j]) {
                        weghtB[j] += min;
                    }
                }
            }
        }

        return result;
    }

    async getRecomEquips() {
        let slotInfo = await this.getSlotInfo();

        let heroes = [];
        let equips = [];

        let equipOwners = {};

        for (let country of slotInfo.countries) {
            for (let hero of country.heroes) {
                heroes.push(hero);

                if (hero.equip) {
                    equips.push(hero.equip);
                    equipOwners[hero.equip.id] = hero;
                }
            }
        }

        let eps = await this.getEquips();

        for (let equip of eps) {
            equips.push(equip);
        }

        let size = Math.max(heroes.length, equips.length);

        let weights = new Array(size);
        for (let i = 0; i < size; i++) {
            weights[i] = new Array(size);

            for (let j = 0; j < size; j++) {
                weights[i][j] = 0;
            }
        }

        for (let i = 0; i < size; i++) {
            let hero = heroes[i];
            if (!hero || hero.heroCardId == "0") {
                continue;
            }

            let weightMin = 0;
            if (hero.locked && hero.equip) {
                let buffer = hero.equip.buffer +
                    (hero.equip.specialEnabled ? hero.equip.specialBuffer : 0);
                weightMin = Math.round(hero.equiplessFight * buffer * 10000) + 1;
            }

            for (let j = 0; j < size; j++) {
                let equip = equips[j];
                let weight = 0;

                if (equip) {
                    let equipInfo = this.getEquipInfo(equip.id, hero);
                    let buffer = equipInfo.buffer +
                        (equipInfo.specialEnabled ? equipInfo.specialBuffer : 0);
                    weight = Math.round(hero.equiplessFight * buffer * 10000);

                    if (hero.equip && hero.equip.id == equip.id) {
                        weight += 1;
                    }
                }

                if (weight < weightMin) {
                    weight = -10000000000;
                }

                weights[i][j] = weight;
            }
        }

        let result = this.km(weights);
        let rst = [];

        let countBuffer = 1 + Math.floor(slotInfo.heroCount / 8) * 0.05;

        for (let i = 0; i < size; i++) {
            let equip = equips[i];
            if (!equip) {
                continue;
            }

            let to = heroes[result[i]];
            if (!to || to.heroCardId == "0") {
                continue;
            }

            let from = equipOwners[equip.id];
            if (from && from.heroCardId == to.heroCardId) {
                continue;
            }

            let item = {
                equip: equip,
                from: from,
                to: to,
            };

            if (to.locked) {
                item.first = true;
            }

            let fromFight = 0;
            if (from) {
                let equipInfo = this.getEquipInfo(equip.id, from);
                let buffer = equipInfo.buffer +
                    (equipInfo.specialEnabled ? equipInfo.specialBuffer : 0);

                fromFight = Math.floor(from.equiplessFight * buffer);
            }

            let equipInfo = this.getEquipInfo(equip.id, to);
            let buffer = equipInfo.buffer +
                (equipInfo.specialEnabled ? equipInfo.specialBuffer : 0);

            let toFight = Math.floor(to.equiplessFight * buffer);

            item.deltaFight = Math.floor((toFight - fromFight) * countBuffer);

            rst.push(item);
        }

        return rst;
    }

    async getExchangeConfigs(address) {
        return await this.call({
            abi: ABIS.HeroShopScoreExchange,
            address: address,
            method: "getExchangeConfigs",
        });
    }

    async getBuyCounts(address, indexes) {
        return await this.call({
            abi: ABIS.HeroShopScoreExchange,
            address: address,
            method: "getBuyCounts",
            args: [indexes],
        });
    }

    async getCoolDownDuration(address) {
        return await this.call({
            abi: ABIS.WorldBoss,
            address: address,
            method: "coolDownDuration",
        });
    }

    async getClearCost(address) {
        return await this.call({
            abi: ABIS.WorldBoss,
            address: address,
            method: "getClearCost",
            args: [this.account],
        });
    }

    async getWorldBossInfo(address) {
        return await this.call({
            abi: ABIS.WorldBoss,
            address: address,
            method: "getUserInfo",
            args: [this.account],
        });
    }

    async getWorldBossInfo2(address) {
        return await this.call({
            abi: ABIS.WorldBoss3,
            address: address,
            method: "getUserInfo",
            args: [this.account],
        });
    }

    async getScoreReward(address) {
        return await this.call({
            abi: ABIS.WorldBoss,
            address: address,
            method: "getScoreReward",
            args: [this.account],
        });
    }

    async getScoreReward2(address) {
        return await this.call({
            abi: ABIS.WorldBoss2,
            address: address,
            method: "getScoreRewards",
            args: [this.account],
        });
    }

    async getScoreConfigs(address) {
        return await this.call({
            abi: ABIS.WorldBoss,
            address: address,
            method: "getScoreConfigs",
        });
    }

    async getScoreConfigs2(address) {
        return await this.call({
            abi: ABIS.WorldBoss2,
            address: address,
            method: "getScoreConfigs",
        });
    }

    async clearCoolDown(address) {
        await this.send({
            abi: ABIS.WorldBoss,
            address: address,
            method: "clearCoolDown",
        });
    }

    async attack(address) {
        await this.send({
            abi: ABIS.WorldBoss,
            address: address,
            method: "attack",
        });
    }

    async getFightLock(address) {
        return await this.call({
            abi: ABIS.Slot,
            address: address,
            method: "fightLocks",
            args: [this.account],
        });
    }

    async lockHeroes(address, heroes) {
        let hrs = [];
        for (let hero of heroes) {
            hrs.push((hero.country << 24) | hero.order);
        }

        await this.send({
            abi: ABIS.Slot,
            address: address,
            method: "lockHeroes",
            args: [hrs],
        });
    }

    async unlockHeroes(address, deadline, heroes, v, r, s) {
        let hrs = [];
        for (let hero of heroes) {
            hrs.push((hero.country << 24) | hero.order);
        }

        await this.send({
            abi: ABIS.Slot,
            address: address,
            method: "unlockHeroes",
            args: [deadline, hrs, v, r, s],
        });
    }

    async getSeasonReward(address, season) {
        return await this.call({
            abi: ABIS.RewardStation,
            address: address,
            method: "getUserInfo",
            args: [season, this.account],
        });
    }

    async pickSeasonReward(address, season) {
        await this.send({
            abi: ABIS.RewardStation,
            address: address,
            method: "pickReward",
            args: [season],
        });
    }

    async getAvatar(address) {
        return await this.call({
            abi: ABIS.Slot,
            address: address,
            method: "getAvatar",
            args: [this.account],
        });
    }

    async setAvatar(address, avatar) {
        await this.send({
            abi: ABIS.Slot,
            address: address,
            method: "setAvatar",
            args: [avatar],
        });
    }

    async buyEquipScoreRarity(address, rarity, base, upgrade, quantity) {
        await this.send({
            abi: ABIS.EquipShopScoreRarity,
            address: address,
            method: "buy",
            args: [rarity, base, upgrade, quantity],
        });
    }

    async buyBattleTicket(quantity) {
        let money = TKConfig.TOKEN.STK;
        let price = new BigInteger(this.toReal(3, money.decimal));
        let amount = price.multiply(new BigInteger(quantity.toString()));

        let memo = {
            name: "battle ticket",
            quantity: Number(quantity),
        };

        await this.send({
            abi: ABIS.Canteen,
            address: this.addrCanteen,
            method: "buy",
            args: [money.address, amount.toString(), JSON.stringify(memo)],
        });
    }

    async buyWorldBossSkill(skills, amount) {
        let money = TKConfig.TOKEN.STK;

        let memo = {
            name: "worldboss skill",
            skills: skills,
        };

        await this.send({
            abi: ABIS.Canteen,
            address: this.addrCanteen,
            method: "buy",
            args: [money.address, amount.toString(), JSON.stringify(memo)],
        });
    }

    async getPickeds(address) {
        return await this.call({
            abi: ABIS.DailyTask,
            address: address,
            method: "getPickeds",
            args: [this.account, [1, 2]],
        });
    }

    async pickTaskReward(address, task, v, r, s) {
        await this.send({
            abi: ABIS.DailyTask,
            address: address,
            method: "pickReward",
            args: [task, v, r, s],
        });
    }

    async getUnpickedReward(address) {
        return await this.call({
            abi: ABIS.OneVsOne,
            address: address,
            method: "userRewards",
            args: [this.account],
        });
    }

    async getVotes(address) {
        return await this.call({
            abi: ABIS.Vote,
            address: address,
            method: "getVotes",
        });
    }

    async getVoteUserInfo(address) {
        return await this.call({
            abi: ABIS.Vote,
            address: address,
            method: "users",
            args: [this.account],
        });
    }

    async vote(address, target, quantity) {
        await this.send({
            abi: ABIS.Vote,
            address: address,
            method: "vote",
            args: [target, quantity],
        });
    }
}

window.threeKingdoms = new ThreeKingdoms();
